yo, what's up
Ramda 是一個 Functional Programming 的函式庫,而 Ramda 的所有函式都有自帶 currying.
筆者一開始學 Functional Programming 的時候,覺得 Ramda 跟 lodash 其實很像阿,一度非常不適應!!而且lodash 的 get 這麼好用,為何要改用 path 跟 prop ,超難用的。
到最後才發現,其實 Ramda 有很多非常好用的功能是 lodash/fp 沒有的,如 transduce
跟 lens
等, 且所有函式都是 data last 以及自帶 currying!
今天要介紹一下 Ramda 裡一些實用的函式
可以看到上圖,R.converge(Fn, [fn1, fn2])
,data 會傳入 fn1, fn2 進行運算,最後作為 Fn 的參數,再運算出最終結果。
舉例,現在有一個需求,是需要將 API 回傳的格式,用 pdfmake 轉成 pdf 檔,所以我們需要做一層 refine 層
API format
[
{
"albumId": 1,
"id": 1,
"title": "accusamus beatae ad facilis cum similique qui sunt",
"url": "https://via.placeholder.com/600/92c952",
"thumbnailUrl": "https://via.placeholder.com/150/92c952"
}
]
pdfmake format
{
content: [
{
style: 'tableExample',
table: {
headerRows: 1,
body: [
[
{text: 'albumId', style: 'tableHeader'},
{text: 'id', style: 'tableHeader'},
{text: 'title', style: 'tableHeader'},
{text: 'url', style: 'tableHeader'},
{text: 'thumbnailUrl', style: 'tableHeader'}
],
[
1,
1,
"accusamus beatae ad facilis cum similique qui sunt",
"https://via.placeholder.com/600/92c952",
"https://via.placeholder.com/150/92c952"
],
]
},
layout: 'lightHorizontalLines'
}
]
}
讀者們可以練習看看
此時我們可以透過 R.converge
去做 refine
const format = val => ({ text: val, style: 'tableHeader' })
const refine = R.converge(
R.pair,
[
R.compose(R.map(format), R.keys, R.head),
R.map(R.values)
]
);
const toPDFFormat = data => ({
content: [
{
style: 'tableExample',
table: {
headerRows: 1,
body: refine(data)
},
layout: 'lightHorizontalLines'
}
]
})
用圖說明上面的流程,首先 data 會分別傳入 R.compose(R.map(format), R.keys, R.head)
以及 R.map(R.values)
進行運算,將 key 與 value 改寫跟取出成 pdfmake 所要的格式, 最終再進行合併。
identity
, 在 Functional Programming 是一個使用頻率很高的函式,實作也非常簡單
const identity = x => x;
對,沒錯,就是將傳入的參數原封不動的回傳,想必讀者們可能會開始納悶,這個函式到底可以用在那些地方。
筆者一開始看到 identity
是在 Functional-Light-JS,其範例是將其作為 predicate 函式,舉例
import * as R from 'ramda'
const wordArr = ['', 'hello', 'world'];
wordArr.filter(R.identity); // ['hello', 'world']
而以下是筆者在專案中使用到的情境
在進行鏈式 (chainable) 寫法時,非常適合將函式 default value 設定為 identity
舉例來說,現在有查詢使用者資料的模組,此一模組負責驗證,送出請求...等, 而所有用到此一業務邏輯的頁面都會用此一模組
const queryUser = (url, schema, options) => {
/** ... other logic ... */
const submitProcess = (data) =>
schema
.validate(data)
.then(d =>
fetch(url, { body: JSON.stringify(d), method: 'POST'})
)
.then(successHandler)
.catch(console.error)
/** ... other logic ... */
return {
submit: submitProcess
}
}
假設現在想要在驗證結束後,送出請求前對送出資料進行 refine, 這時候我們就可以在多新增一條 .then(refineAfterValidated)
,並且為了不要動到其他已用此一模組的服務,我們就可以給refineAfterValidated
預設為 identity ,這樣既不會影響現有服務,也可以擴充新需求。
const queryUser = (url, schema, options) => {
const { refineAfterValidated = identity } = options;
/** ... other logic ... */
const submitProcess = (data) =>
schema
.validate(data)
.then(refineAfterValidated)
.then(d =>
fetch(url, { body: JSON.stringify(d), method: 'POST'})
)
.then(successHandler)
.catch(console.error)
/** ... other logic ... */
return {
submit: submitProcess
}
}
這個概念其實源自於 Functional Programming 的 Applying,是一個非常重要的概念,也會在之後的文章講到。
現在讀者們可以思考一件事情,當兩組 Array 內的值要進行相加時,我們會怎麼做?
const add = (x, y) => x + y;
add(Array.of(1), Array.of(1)) // "11"
結果出現了 11
,為什麼呢? 因為 Array 內值的就像是在一個 container 內,我們必須將它取出才能進行運算,所以我們需要先將值透過 map 取出來,才能進行運算
Array.of(1).map(x => Array.of(1).map(y => add(x, y))) // [[2]]
// same as
[1].map(x => [1].map(y => add(x, y))
但問題似乎還沒有解決,現在的值反而被兩層 Container(Array) 包覆住了,所以我們需要壓平它
Array.of(1).flatMap(x => Array.of(1).map(y => add(x, y))) // [2]
這樣看起來非常的複雜,lift 就是用來解決這個問題的勇者,其概念就是 Applying,之後會在 Applying 的時候講解!
import * as R from 'ramda'
R.lift(R.add)([1], [1]) // [2]
這樣是不是變得非常乾淨!!!!!
但要注意放入
lift
跟liftN
的函式都必須是 currying 化的函式!
而另外一個實用的小技巧就是有需要進行排列組合的運算時,也可以使用 lift
,
const color = ['Black', 'White'];
const size = ['S', 'M', 'L'];
const result = lift(pair)(color, size)
// [["Black", "S"], ["Black", "M"], ["Black", "L"], ["White", "S"], ["White", "M"], ["White", "L"]]
也可以用相同概念 liftN
去改寫
const color = ['Black', 'White'];
const size = ['S', 'M', 'L'];
const combine = [color, size]
const result = liftN(combine.length, pair)(...combine)
// [["Black", "S"], ["Black", "M"], ["Black", "L"], ["White", "S"], ["White", "M"], ["White", "L"]]
而其實 liftN
底層時做就是用 ramda 的 ap
跟 map
進行實作,我們就來簡單實作一個簡易版的 liftN
import * as R from 'ramda';
const lift2 = R.curry((g, f1, f2) => R.ap(R.map(g, f1), f2))
const liftN_ = R.curry((arity, fn, list) =>
list.slice(1, arity)
.reduce((acc, val) => ap(acc, val), R.map(fn, list[0])))
liftN_(2, pair, [color, size])
// [["Black", "S"], ["Black", "M"], ["Black", "L"], ["White", "S"], ["White", "M"], ["White", "L"]]
當日常開發中,遇到連續使用 .map
.filter
以及 .reduce
的情境時,就非常適合用 transduce 去進行優化,由於有幾個 .map
跟 .filter
時間複雜度就會多幾個 O(n),而 transduce 就是將其優化到無論現在有幾個 .map
跟 .filter
時間複雜度就是固定的O(n) ,之後會在未來的文章內提到實作方法。
假設我們現在有一組陣列,要乘三後取偶數,原本寫法
const tripleIt = x => x * 3;
const isEven = (num) => num % 2 === 0
const arr = [1, 2, 3, 4]
// 原本寫法
arr.map(tripleIt).filter(isEven) // [6, 12]
用 transduce後寫法
import * as R from 'ramda'
const transducer = R.compose(R.map(tripleIt), R.filter(isEven))
R.transduce(transducer, R.flip(R.append), [], [1, 2, 3, 4]); // [6, 12]
在 Function Composition 的時候有提到如何對 compose 進行 debug
而 tap
就節省了我們寫 log
的時間,所以可以將上次範例改寫
import * as R from 'ramda'
const makeItalianHerbalBreast =
compose(
R.tap(console.log),
wrapIt,
R.tap(console.log),
addFlavor('italianHerbal'),
R.tap(console.log),
grab('breast'),
R.tap(console.log),
)(WHOLE_CHICKEN)
// [ 'leg', 'wing', 'breast', 'buttock', 'breast strips', 'land', 'bug' ]
// breast
// italianHerbal breast
// Wrapped(italianHerbal breast
感謝大家的閱讀!
NEXT: Lense
identity 真的是剛學 FP 時覺得莫名奇妙的東西,但等真的需要用到就知道它的存在價值了XDD
我記得第一次知道 transduce 的意義跟用途也是看大大的部落格文章。大心~
期待後續的分享耶~